/*
 * MIT License
 *
 * Copyright (c) 2024-2048 冰羽
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package cn.star.framework.service;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.star.framework.core.IEntity;
import cn.star.framework.core.IRepository;
import cn.star.framework.core.IService;
import cn.star.framework.core.ITreeEntity;
import cn.star.framework.core.IdEntity;
import cn.star.framework.core.api.Pageable;
import cn.star.framework.sql.Identifier;
import cn.star.framework.sql.standard.Sorter;
import cn.star.framework.sql.standard.Where;
import cn.star.framework.sql.standard.WhereGroup;
import cn.star.framework.strategy.UpdateStrategy;
import cn.star.framework.support.QueryBuilder;
import cn.star.framework.support.convert.FieldConverterCache;
import cn.star.framework.type.OrderType;
import cn.star.framework.type.QueryType;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.persistence.AttributeConverter;
import javax.persistence.Version;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import cn.star.framework.type.BooleanOperator;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;

/**
 * 数据库服务抽象实现<br>
 *
 * @author zhaoweiping
 *     <p style='color: red'>Created on 2024-09-23 11:06:00
 * @since 3.0.0
 */
@Slf4j
public abstract class AbstractService<
        T extends IEntity, Id extends Serializable, R extends IRepository<T, Id>>
    implements IService<T, Id> {

  protected final R repository;

  public AbstractService(R repository) {
    this.repository = repository;
  }

  /**
   * 更新策略
   *
   * @param entity 实体
   * @param strategy {@link UpdateStrategy}
   */
  protected <S extends T> void useUpdateStrategy(S entity, UpdateStrategy strategy) {
    if (strategy == UpdateStrategy.IGNORE_VERSION) {
      try {
        Field[] fields = ReflectUtil.getFields(entity.getClass());
        for (Field field : fields) {
          if (field.getAnnotation(Version.class) != null) {
            T e = this.findById(entity);
            Integer version = (Integer) ReflectUtil.getFieldValue(e, field);
            ReflectUtil.setFieldValue(entity, field, version);
          }
        }
      } catch (Exception e) {
        // 忽略异常
      }
    }
  }

  @Override
  public <S extends T> S save(S entity, UpdateStrategy strategy) {
    this.useUpdateStrategy(entity, strategy);
    return repository.save(entity);
  }

  @Override
  public <S extends T> S save(S entity, UpdateStrategy strategy, boolean flush) {
    this.useUpdateStrategy(entity, strategy);
    return flush ? repository.saveAndFlush(entity) : repository.save(entity);
  }

  /**
   * 更新策略
   *
   * @param entities 实体
   * @param strategy {@link UpdateStrategy}
   */
  protected <S extends T> void useUpdateStrategy(Iterable<S> entities, UpdateStrategy strategy) {
    if (strategy == UpdateStrategy.IGNORE_VERSION && CollectionUtil.isNotEmpty(entities)) {
      for (S entity : entities) {
        this.useUpdateStrategy(entity, strategy);
      }
    }
  }

  @Override
  public <S extends T> List<S> save(Iterable<S> entities, UpdateStrategy strategy) {
    this.useUpdateStrategy(entities, strategy);
    return repository.saveAll(entities);
  }

  @Override
  public List<T> deleteByIds(Iterable<Id> ids) {
    List<T> entities = repository.findAllById(ids);
    this.delete(entities);

    return entities;
  }

  @Override
  public void delete(Iterable<T> entities) {
    repository.deleteAllInBatch(entities);
  }

  @Override
  public List<T> findAll() {
    return repository.findAll();
  }

  @Override
  public List<T> findAll(Specification<T> spec) {
    return repository.findAll(spec);
  }

  @Override
  public List<T> findAll(Specification<T> spec, Sort sort) {
    return repository.findAll(spec, sort);
  }

  @Override
  public long count(Specification<T> spec) {
    return spec == null ? repository.count() : repository.count(spec);
  }

  @Override
  public boolean isExists(Specification<T> spec) {
    return repository.exists(spec);
  }

  @Override
  public boolean isExists(Id id) {
    return repository.existsById(id);
  }

  @Override
  public T findOne(Specification<T> spec) {
    return repository.findOne(spec).orElse(null);
  }

  @Override
  public T findById(Id id) {
    return repository.findById(id).orElse(null);
  }

  /**
   * 获取实体内的主键值集合
   *
   * @param entity {@link T}
   * @return 联合主键
   */
  protected Map<String, Object> getIdMapValues(T entity) {
    Map<String, Object> idMapValues = new HashMap<>(1);
    Field[] fields = ReflectUtil.getFields(entity.getClass());
    for (Field field : fields) {
      if (field.getAnnotation(javax.persistence.Id.class) != null) {
        Object value = ReflectUtil.getFieldValue(entity, field);
        idMapValues.put(field.getName(), value);
      }
    }
    return idMapValues;
  }

  @Override
  @SuppressWarnings({"unchecked", "rawtypes"})
  public T findById(T entity) throws Exception {
    if (entity instanceof IdEntity) {
      return this.findById((Id) ((IdEntity) entity).getId());
    }
    Map<String, Object> idMapValues = this.getIdMapValues(entity);

    if (idMapValues.isEmpty()) {
      return null;
    }

    List<T> entities =
        this.repository.findAll(
            (root, query, cb) -> {
              List<Predicate> predicates = new ArrayList<>(idMapValues.size());
              idMapValues.forEach((name, id) -> predicates.add(cb.equal(root.get(name), id)));
              return cb.and(ArrayUtil.toArray(predicates, Predicate.class));
            });

    if (CollectionUtil.isEmpty(entities)) {
      return null;
    }
    if (entities.size() > 1) {
      throw new Exception(
          "use primary keys: "
              + idMapValues
              + ", The instance found is not unique: "
              + entities.size());
    }

    return entities.get(0);
  }

  @Override
  public List<T> findAllByIds(Iterable<Id> ids) {
    return repository.findAllById(ids);
  }

  @Override
  public Pageable<T> pageable(Specification<T> spec, int pageNo, int pageSize) {
    return this.pageable(spec, pageNo, pageSize, null);
  }

  @Override
  public Pageable<T> pageable(Specification<T> spec, int pageNo, int pageSize, Sort sort) {
    Page<T> page =
        spec == null
            ? repository.findAll(
                sort == null
                    ? PageRequest.of(pageNo - 1, pageSize)
                    : PageRequest.of(pageNo - 1, pageSize, sort))
            : repository.findAll(
                spec,
                sort == null
                    ? PageRequest.of(pageNo - 1, pageSize)
                    : PageRequest.of(pageNo - 1, pageSize, sort));

    Pageable<T> pageable = new Pageable<>();

    pageable.setPageNo(pageNo);
    pageable.setPageSize(pageSize);
    pageable.setData(page.getContent());
    pageable.setTotal(page.getTotalElements());

    return pageable;
  }

  // QueryBuilder dynamic query

  private <X> Path<X> wrapper(
      Identifier identifier, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
    // TODO 函数暂未支持
    return root.get(identifier.getName());
  }

  private void setSelectClause(
      QueryBuilder builder, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
    List<Identifier> identifiers = builder.getQueryColumns();
    if (CollectionUtil.isEmpty(identifiers)) {
      return;
    }

    List<Selection<?>> selections = new ArrayList<>(identifiers.size());
    for (Identifier identifier : identifiers) {
      selections.add(this.wrapper(identifier, root, query, cb));
    }

    query.multiselect(selections);
  }

  private void setSelectGroupByClause(
      QueryBuilder builder, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
    // TODO 暂未支持
  }

  private void setSelectOrderByClause(
      QueryBuilder builder, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
    List<Sorter> list = builder.getSorters();
    if (CollectionUtil.isEmpty(list)) {
      return;
    }

    List<Order> orders = new ArrayList<>();
    for (Sorter sorter : list) {
      List<Identifier> identifiers = sorter.getNames();
      for (Identifier identifier : identifiers) {
        Expression<?> x = this.wrapper(identifier, root, query, cb);
        orders.add(sorter.getType() == OrderType.ASC ? cb.asc(x) : cb.desc(x));
      }
    }

    query.orderBy(orders);
  }

  /** 条件元组 */
  private static class WhereTuple {
    @Getter @Setter private Predicate where;
    @Getter @Setter private BooleanOperator operator;
    @Getter @Setter private WhereGroup whereGroup;

    public WhereTuple(Predicate where, BooleanOperator operator, WhereGroup whereGroup) {
      this.where = where;
      this.operator = operator;
      this.whereGroup = whereGroup;
    }
  }

  private <Y> Predicate builder(
      QueryType type, List<Y> values, CriteriaBuilder cb, Expression<? extends Y> x) {
    final int maxSize = 1000;
    if (type == QueryType.IN || type == QueryType.NOT_IN) {
      if (values.size() >= maxSize) {
        Predicate[] predicates = new Predicate[values.size()];
        for (int i = 0; i < values.size(); i++) {
          predicates[i] =
              type == QueryType.IN ? cb.equal(x, values.get(i)) : cb.notEqual(x, values.get(i));
        }
        return type == QueryType.IN ? cb.or(predicates) : cb.and(predicates);
      } else {
        CriteriaBuilder.In<Y> in = cb.in(x);
        values.forEach(in::value);

        return type == QueryType.IN ? cb.in(in) : cb.not(in);
      }
    }
    throw new RuntimeException("not support type " + type);
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private <Y extends Comparable<? super Y>> Predicate builder(
      Where where, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
    AttributeConverter<Object, Object> converter =
        FieldConverterCache.getConverterInstance(where.getName().getField());
    Object value =
        where.getColumnValue(converter == null ? null : converter::convertToEntityAttribute);

    QueryType type = where.getType();
    Expression x = this.wrapper(where.getName(), root, query, cb);

    if (type == QueryType.EQ) {
      return cb.equal(x, value);
    } else if (type == QueryType.NE) {
      return cb.notEqual(x, value);
    } else if (type == QueryType.GT) {
      return cb.greaterThan(x, (Y) value);
    } else if (type == QueryType.GE) {
      return cb.greaterThanOrEqualTo(x, (Y) value);
    } else if (type == QueryType.LT) {
      return cb.lessThan(x, (Y) value);
    } else if (type == QueryType.LE) {
      return cb.lessThanOrEqualTo(x, (Y) value);
    } else if (type == QueryType.BETWEEN) {
      List<Y> values = (List<Y>) value;
      return cb.between(x, values.get(0), values.get(1));
    } else if (type == QueryType.NOT_BETWEEN) {
      List<Y> values = (List<Y>) value;
      // (-∞, startValue]
      Predicate lt = cb.lessThanOrEqualTo(x, values.get(0));
      // [endValue, +∞)
      Predicate gt = cb.greaterThanOrEqualTo(x, values.get(1));
      return cb.or(lt, gt);
    } else if (type == QueryType.LIKE) {
      return cb.like(x, "%" + value + "%");
    } else if (type == QueryType.NOT_LIKE) {
      return cb.notLike(x, "%" + value + "%");
    } else if (type == QueryType.LIKE_LEFT) {
      return cb.like(x, value + "%");
    } else if (type == QueryType.LIKE_RIGHT) {
      return cb.like(x, "%" + value);
    } else if (type == QueryType.NULL) {
      return cb.isNull(x);
    } else if (type == QueryType.NOT_NULL) {
      return cb.isNotNull(x);
    } else if (type == QueryType.IN) {
      // 在Oracle中，当in语句中的值超过1000个时报错 使用 or 语句 (?)，此时索引会失效
      List<Y> values = (List<Y>) value;
      return this.builder(type, values, cb, x);
    } else if (type == QueryType.NOT_IN) {
      List<Y> values = (List<Y>) value;
      return this.builder(type, values, cb, x);
    } else {
      throw new RuntimeException("Not support type: " + type);
    }
  }

  private Predicate builder(
      List<Where> wheres, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
    // default AND, conjunction
    Predicate predicate = cb.conjunction();
    for (Where where : wheres) {
      predicate.getExpressions().add(this.builder(where, root, query, cb));
    }

    return predicate;
  }

  private List<WhereTuple> convert(
      QueryBuilder builder, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
    List<WhereGroup> whereGroups = builder.getWhereGroups();

    List<WhereTuple> predicates = new ArrayList<>();
    for (WhereGroup whereGroup : whereGroups) {
      Predicate where = this.builder(whereGroup.getWheres(), root, query, cb);
      predicates.add(new WhereTuple(where, whereGroup.getOperator(), whereGroup));
    }

    return predicates;
  }

  private void connect(Predicate where, CriteriaBuilder cb, WhereTuple tuple) {
    if (tuple.getOperator() == BooleanOperator.AND) {
      where.getExpressions().add(cb.and(tuple.getWhere()));
    } else {
      where.getExpressions().add(cb.or(tuple.getWhere()));
    }
  }

  private Predicate builder(
      QueryBuilder builder, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {

    // 查询
    this.setSelectClause(builder, root, query, cb);
    // 分组
    this.setSelectGroupByClause(builder, root, query, cb);
    // 排序
    this.setSelectOrderByClause(builder, root, query, cb);

    Predicate where = cb.conjunction();
    List<WhereTuple> tuples = this.convert(builder, root, query, cb);
    for (WhereTuple tuple : tuples) {
      this.connect(where, cb, tuple);
    }

    return where;
  }

  private Specification<T> builder(QueryBuilder builder) {
    return (root, query, cb) -> this.builder(builder, root, query, cb);
  }

  @Override
  public List<T> findAll(QueryBuilder builder) {
    Specification<T> spec = this.builder(builder);

    return this.findAll(spec);
  }

  @Override
  public Pageable<T> pageable(QueryBuilder builder, int pageNo, int pageSize) {
    Specification<T> spec = this.builder(builder);

    return this.pageable(spec, pageNo, pageSize);
  }

  // Function

  @SuppressWarnings("unchecked")
  protected <Tree extends ITreeEntity<Id, Tree>> List<Tree> getTreeChildren(Id id) {
    // support ITreeEntity#getParentId
    String defaultParentIdName = "parentId";
    return (List<Tree>)
        this.findAll((root, query, cb) -> cb.equal(root.get(defaultParentIdName), id));
  }

  protected <Tree extends ITreeEntity<Id, Tree>> void setTreeChildren(Tree entity) {
    List<Tree> children = this.getTreeChildren(entity.getId());
    if (CollectionUtil.isNotEmpty(children)) {
      entity.setChildren(children);
      for (Tree x : children) {
        this.setTreeChildren(x);
      }
    }
  }

  protected <Tree extends ITreeEntity<Id, Tree>> void setTreeChildren(
      Tree entity, List<Tree> list) {
    if (CollectionUtil.isEmpty(list)) {
      this.setTreeChildren(entity);
      return;
    }
    Id id = entity.getId();
    List<Tree> children =
        list.stream().filter(i -> id.equals(i.getParentId())).collect(Collectors.toList());
    if (CollectionUtil.isNotEmpty(children)) {
      entity.setChildren(children);
      children.forEach(i -> this.setTreeChildren(i, list));
    }
  }

  /**
   * 获取一个整树<br>
   *
   * <ul>
   *   <li>默认使用丢归查询，按照树是一个数据量非常大的结构处理
   *   <li>树数据量较小推荐使用一次性查询，在内存中构建树
   * </ul>
   *
   * @param oneQuery
   * @return
   * @param <Tree>
   */
  @SuppressWarnings("all")
  protected <Tree extends ITreeEntity<Id, Tree>> List<Tree> buildTree(boolean oneQuery) {
    String defaultParentIdName = "parentId";

    List<Tree> list =
        (List<Tree>)
            (oneQuery
                ? this.findAll()
                : this.findAll((root, query, cb) -> cb.isNull(root.get(defaultParentIdName))));

    List<Tree> root =
        list.stream().filter(i -> i.getParentId() == null).collect(Collectors.toList());

    root.forEach(i -> this.setTreeChildren(i, oneQuery ? list : null));

    return root;
  }
}
